<?php

Raise::load('core.RaiseObject');
Raise::load('core.collection.RaiseTypedCollection');
Raise::load('core.collection.RaiseCollection');
Raise::load('core.io.file.RaisePath');
Raise::load('core.asset.RaiseAssetType');
Raise::load('core.asset.RaiseAsset');
Raise::load('core.helper.convert.RaiseConvert');

/**
 * RaiseAssetContainer class
 * A container managing groups of assets
 *
 * @author Sam Yong
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package phpraise.core.asset
 * @since 1.2
 */
class RaiseAssetContainer extends RaiseObject {
    
    /**
     *
     * @var RaiseTypedCollection
     */
    private $groups;
    
    /**
     * Create a new RaiseAssetContainer
     */
    public function __construct(){
        $this->groups = new RaiseTypedCollection('RaiseTypedCollection');
    }
    
    /**
     * Add a new asset to the container
     * @param string $key Name / unique identifier of the asset.
     * @param string|RaiseUrl $path The URL / path to the asset
     * @param array|RaiseCollection $dependencies (optional) The dependencies for this asset. If left unset, the asset is assumed to have no dependencies
     * @param string $type (optional) The asset type. If left unset, the type will be detected from the path's extension.
     */
    public function add($key, $path, $dependencies = null, $type = null){
        if(is_object($path)){
            if($path instanceof RaiseUrl){
                $path = $path->path();
            }
            $path = RaiseConvert::to(RaiseConvert::TO_STRING, $path);
        }
        if(!$type){
            $type = $this->detectType($path);
        }
        if($dependencies && !($dependencies instanceof RaiseCollection)){
            $dependencies = new RaiseCollection($dependencies);
        }
        $asset = new RaiseAsset($key, $path, $type, $dependencies);
        if(!$this->groups->keyExists($type)){
            $this->groups->add($type, new RaiseTypedCollection('RaiseAsset'));
        }
        $this->groups->get($type)->add($key, $asset);
    }
    
    /**
     * Get the output for CSS assets in this container
     * @return string
     */
    public function style(){
        return $this->get(RaiseAssetType::CSS);
    }
    
    /**
     * Get the output for the JavaScript assets in this container
     * @return string
     */
    public function script(){
        return $this->get(RaiseAssetType::JS);
    }
    
    /**
     * Get the HTML output of a group of assets
     * @param string $name The name of the group, use constants from RaiseAssetType
     */
    public function get($name){
        $this->sort($name);
        $assets = $this->groups->get($name);
        $output = '';
        if($assets instanceof RaiseTypedCollection){
            foreach($assets as $asset){
                $output .= $asset->output();
            }
        }
        return $output;
    }
    
    /**
     * Detect the asset type through the file extension
     * @param string $path
     * @return string One of the constants from RaiseAssetType
     */
    private function detectType($path){
        $ext = strtolower(RaisePath::extension($path));
        switch($ext){
            case 'js':
                return RaiseAssetType::JS;
                break;
            case 'css':
                return RaiseAssetType::CSS;
                break;
            case 'ico':
                return RaiseAssetType::FAVICON;
                break;
            case 'rss':
                return RaiseAssetType::RSS;
                break;
        }
        return null;
    }
    
    /**
     * Sort a particular group according to its assets' dependencies
     * @param string $group The name of the group to sort
     */
    private function sort($group){
        $assets = $this->groups->get($group);
        if($assets instanceof RaiseTypedCollection){
            $sorted = array();
            $kAssets = array();
            foreach($assets as $name => $asset){
                $ok = $this->checkDependencies($name, $assets);
                if(!$ok){
                    return;
                }
                $kAssets[$name] = $asset->dependencies()->toArray();
            }
            while(count($kAssets) > 0){
                foreach($kAssets as $name => $dep){
                    if(count($dep) == 0){
                        $sorted[$name] = $assets[$name];
                        unset($kAssets[$name]);
                    }else{
                        foreach($dep as $x => $depName){
                            if(array_key_exists($depName, $sorted)){
                                unset($kAssets[$name][$x]);
                            }
                        }
                    }
                }
            }
            $assets->clear();
            foreach($sorted as $k => $v){
                $assets->add($k, $v);
            }
        }
    }
    
    /**
     * Check an asset for dependency problems
     * @param string $key Name of the asset to check
     * @param array|RaiseCollection $assets Collection of assets
     * @return boolean
     */
    private function checkDependencies($key, $assets){
        $dep = $assets[$key]->dependencies();
        if($dep->count() == 0){
            return true;
        }
        foreach($dep as $name){
            if($name == $key){
                throw new RaiseSelfReferenceException('Asset \'' . $key . '\' has a self-referencing dependency.');
                return false;
            }
            if($assets instanceof RaiseCollection && $assets->keyExists($name) || array_key_exists($name, $assets)){
                $depdep = $assets[$name]->dependencies();
                foreach($depdep as $name2){
                    if($name2 == $key){
                        throw new RaiseCircularDependencyException('Asset \'' . $key . '\' has a circular dependency with asset \'' . $name . '\'.');
                        return false;
                    }
                }
            }else{
                throw new RaiseMissingDependencyException('Asset \'' . $key . '\' has a missing dependency \'' . $name . '\'.');
                return false;
            }
        }
        return true;
    }
    
}